-
Notifications
You must be signed in to change notification settings - Fork 536
Allow for precompiles to have arbitrary addresses, potentially more than one. #1016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
frame/evm/src/lib.rs
Outdated
| pub type AccountStorages<T: Config> = | ||
| StorageDoubleMap<_, Blake2_128Concat, H160, Blake2_128Concat, H256, H256, ValueQuery>; | ||
|
|
||
| /// Allows for precompiles to have arbitrary addresses, potentially more than one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In which cases do you use the multiple addresses for a particular precompile? If the address of the a precompile changes, is it enough to just update the address is enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think mostly something like ERC20/asset. You can have lots of contracts of the same code.
If the address of the a precompile changes, is it enough to just update the address is enough?
No, because in this case the address will have their associated storages, which differentiate them.
And this is supposed not to happen -- a precompile's label is to remain static.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In which cases do you use the multiple addresses for a particular precompile?
This change is in the context of a new feature that will potentially be upstreamed later. We want to have specific precompiles that use traits like frame_support::traits::tokens::fungibles::*. For example, an ERC20-compatible precompile that uses pallet-assets to manage the tokens. In this case, different precompile addresses (that are associated with the Fungibles label) will map to different AssetIds.
But for most use-cases where a single precompile address is sufficient, this won't be really useful. In those cases, just associate a single address (potentially hardcoded) to the precompile label and that's it.
If the address of the a precompile changes, is it enough to just update the address is enough?
I'm not sure I fully understand the question. With this implementation, you can't really "change" the address of a precompile. You can add a new precompile address with the same label, which would now make two instances of the same precompile. You can later remove the first address. Whatever dApp that expects the old precompile address would stop working at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another point worth mentioning is that using this feature (on-chain precompile addresses) is totally optional.
You could still implement your own PrecompileSet::execute() so that it only executes precompiles with hardcoded addresses, the same way FrontierPrecompiles<R> does in its current form:
https://github.com/paritytech/frontier/blob/58c568964dc32dd96153b7d113ac2df58d27de2b/template/runtime/src/precompiles.rs#L30-L47
A hybrid solution mixing both approaches (hardcoded + on-chain precompile addresses) is also possible.
The only breaking changes that this PR imposes is in case some runtime eventually decides to opt-in to on-chain precompile addresses via pallet-evm-precompile, a storage migration is necessary to populate storage with the previously hardcoded addresses.
frame/evm/src/lib.rs
Outdated
| #[pallet::storage] | ||
| #[pallet::getter(fn precompiles)] | ||
| pub type Precompiles<T: Config> = | ||
| StorageDoubleMap<_, Blake2_128Concat, Vec<u8>, Blake2_128Concat, H160, (), OptionQuery>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this pr is finally acceptable, consider changing the () to bool?
This way, you can choose to enable/disable one of the precompile's addresses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm why do we need double storage map here? I think just a storage map mapping H160 to label would be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sorpaas I switched to StorageMap.
But then I was thinking: wouldn't it be useful to use StorageDoubleMap and have an index for each H160 address?
k1:H160k2:PrecompileLabelv:PrecompileIndex
where PrecompileIndex is a Config type, with same traits as AssetId
that would avoid the need for using #[precompiles::precompile_set] & #[precompile::discriminant] to distinguish between precompiles within the same label (which btw is Moonbeam's approach for their assets-erc20 precompiles cc @nanocryk )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That can be useful, but I don't see why we need double storage map. We can have H160 -> (PrecompileLabel, PrecompileIndex) map, and that should be sufficient. Instead of PrecompileIndex, we should allow arbitrary data to be passed to the executor with the type controllable by the config.
I honestly start to think that all this probably belongs to a new pallet. Apart from all the scaffolding, it should be a trivial change. We have a new pallet pallet-evm-precompile and move the following to it: a) this precompile storage map, b) the add/remove precompile dispatchable. Then we implement PrecompileSet on Pallet struct. Any users who have imported the new pallet can then set the PrecompileSet config in pallet-evm to this new pallet.
The advantage of this is that anyone who wants not to use precompile label can stop worrying about it, and don't need to care about an unused precompile storage map in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool, I'll refactor the changes into a separate pallet-evm-precompile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we leave template using the old FrontierPrecompiles as PrecompileSet, or the new one (pallet-evm-precompile)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some experiments and it turns out having an on-chain PrecompileIndex is not really useful.
impl<R> PrecompileSet for OnChainPrecompiles<R>
where
R: pallet::Config + pallet_evm::Config,
{
fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
match handle.code_address() {
// Ethereum precompiles :
a if &Precompiles::<R>::get(a).0[..] == b"PrecompileX" => {
let precompile_index = Precompiles::<R>::get(a).1; // this is useless
Some(PrecompileX::execute(handle))
}
...
even though I can read precompile_index, I have no means to pass it to PrecompileX::execute(handle).
It seems the only available approach is using Moonbeam's #[precompiles::precompile_set] & #[precompile::discriminant].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can modify that trait to take on precompile_index though.
But if you want, let's do that in a separate PR. This precompile_index is best added as a separate storage item (because large storage, which can happen with arbitrary data, takes longer to retrieve).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we leave template using the old FrontierPrecompiles as PrecompileSet, or the new one (pallet-evm-precompile)?
Please make the template use the new one. One of the use for the template is to test our all pallets within this project, so it should include as many pallets as possible.
…le + remove_precompile dispatchables
fca59a0 to
584f5a2
Compare
04c4a73 to
d453727
Compare
d453727 to
093d3a9
Compare
| Self(Default::default()) | ||
| } | ||
| } | ||
| impl<R> PrecompileSet for OnChainPrecompiles<R> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can directly impl<T: Config> PrecompileSet for Pallet<T>. That will allow the interface to be much nicer.
To do that you need to create another trait whose purpose is to map labels to executions, something like this:
trait PrecompileLabelSet {
fn execute(&self, label: &PrecompileLabel, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;
}
Then add that to Config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pallet-evm has the following types in its Config trait:
type PrecompilesType: PrecompileSet;
type PrecompilesValue: Get<Self::PrecompilesType>;
as pointed out by @kianenigma , it seems that impl<T: Config> PrecompileSet for Pallet<T> is not doable, because pallet structs are not meant to be instantiated: https://substrate.stackexchange.com/questions/7579/how-to-implement-the-get-trait-for-a-pallett?noredirect=1#comment7602_7579
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd still prefer that we use the pallet name directly instead of creating another custom type, to keep things consistent everywhere.
To do this we need to remove the self attribute from PrecompileSet. This is trivial on type level:
trait Precompiles {
fn execute(arg1, arg2) -> res;
}
trait PrecompileSet {
fn execute(&self, arg1, arg2) -> res;
}
impl<T: PrecompileSet> Precompiles for Get<T> {
fn execute(arg1, arg2) -> res {
T::get().execute(arg1, arg2)
}
}
Then on config:
type Precompiles: Precompiles;
However, on code level we uses in a lot of places actual references of precompiles, which makes things slightly complicated.
evm and pallet-evm had different design goals. In evm we'd actually want the reference thing because precompile set can be dynamic. And people should be able to change the set without recompiling their code. In pallet-evm, this is not needed at all because all the storage accesses (which is the only thing that makes things dynamic) is entirely static.
We probably have other type workarounds, so unless you have better ideas, please temporarily do not work on this until we figure something out (or decide to use the old wrapper type method).
|
I would likely discourage to store the mapping in Substrate storage unless you want to be able to change the addresses without runtime upgrades. You do a lot of storage reads which have non negligible costs. If you don't need to change addresses outside of runtime upgrades, you should make your own enum representing the precompiles with a method If you need this storage I would advice renaming it to |
|
@nanocryk The case is when you have millions of precompiles around and when most of them are of the same code (think ERC20). It's impossible to keep everything in memory. That's also why this is designed as a separate pallet, because not everyone needs them. |
The main modification is the addition of a newStorageDoubleMapintopallet-evm:edit: this PR adds a new
pallet-evm-precompile, which allows using Precompiles with arbitrary addresses, potentially more than one.A
StorageMapkeeps track of the Precompiles on-chain, where:H160PrecompileLabelhttps://github.com/paritytech/frontier/blob/e13ce6c611771f5b436d1e3cff89c013853f4b71/frame/evm-precompile/src/lib.rs#L147-L150
A
PrecompileLabeldetermines which functionality the Precompile has. It is declared as aBoundedVec<u8, ConstU32<32>>, which means the user is free to choose a label (e.g.:b"Sha3FIPS512") that's up-to 32 bytes long:https://github.com/paritytech/frontier/blob/e13ce6c611771f5b436d1e3cff89c013853f4b71/frame/evm-precompile/src/lib.rs#L46
OnChainPrecompilesimplements thePrecompileSettrait, where the Precompile addresses are routed to the appropriatePrecompile::executeimplementation according to the on-chan mapping:https://github.com/paritytech/frontier/blob/e13ce6c611771f5b436d1e3cff89c013853f4b71/frame/evm-precompile/src/lib.rs#L72-L107